﻿#region License

/*
    Copyright (c) 2012 Joseph Haluska
    This source is subject to the Microsoft Public License (Ms-PL).
    Please see http://www.microsoft.com/en-us/openness/licenses.aspx for details.
    All other rights reserved.
*/

#endregion License

#define BREAK_IN_EXCEPTION_REPORTER //stops an attached debugger in ReportException(...) and ShutdownProgram()

//#define DISABLE_EXCEPTION_REPORTER //disables ReportException(...)

#if DISABLE_EXCEPTION_REPORTER
#    warning ExceptionReporter is disabled
#endif

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using NetExceptionReporter.Extensions;
using NetExceptionReporter.Utilities;

namespace NetExceptionReporter
{
    /// <summary>
    /// Provides a way for the user to report report exceptions.
    /// </summary>
    /// <remarks>
    /// Examples of how to use this exception reporter are provided in Readme.md.
    /// </remarks>
    [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)]
    public class ExceptionReporter
    {
        #region Fields

        private static int _reportWindowCount;
        private List<Exception> _reportedExceptions = new List<Exception>();

        #endregion Fields

        #region Properties

        public static string EntryAssemblyPath
        {
            get { return new Uri(Assembly.GetEntryAssembly().CodeBase).LocalPath; }
        }

        private IExceptionWindowBehavior _behavior;

        public IExceptionWindowBehavior Behavior
        {
            get { return _behavior; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("Behavior");

                _behavior = value;
            }
        }

        #endregion Properties

        #region Constructor

        /// <summary>
        /// Catches uncaught exceptions.
        /// </summary>
        public ExceptionReporter()
        {
            Behavior = new DefaultExceptionWindowBehavior();
        }

        /// <summary>
        /// Catches uncaught exceptions.
        /// </summary>
        /// <param name="reporter">Provides a custom exception reporting behavior.</param>
        public ExceptionReporter(IExceptionWindowBehavior reporter)
        {
            Behavior = reporter;
        }

        #endregion Constructor

        #region Members

        public void ReportException(Exception reportedException, bool programMustClose = false)
        {
#if BREAK_IN_EXCEPTION_REPORTER
            if (Debugger.IsAttached)
                Debugger.Break();
#endif

#if DISABLE_EXCEPTION_REPORTER
            return;
#else
            try
            {
                if (reportedException == null)
                    throw new ArgumentNullException("reportedException");

                bool canShowExceptionForm = true;

                //ignoring exceptions which are already reported to the user
                foreach (Exception ex in _reportedExceptions)
                {
                    if (ex.IsEquivalent(reportedException))
                    {
                        canShowExceptionForm = false; //we can't just return from the function here because it can lead to a bad crash of the exception reporter if to many unhandled exceptions (specifically of the unhandledException = true type) occur. The reason seems to be we're not showing another form for the unhandled exception, but the thread.sleep wait is still required.. or else the exception will cause the program to crash if we just return.
                        break;
                    }
                }

                if (canShowExceptionForm)
                {
                    bool displayExceptionWindow;
                    Behavior.OnException(reportedException, out displayExceptionWindow);

                    if (displayExceptionWindow)
                    {
                        ExceptionWindowData data = new ExceptionWindowData(reportedException, Behavior, programMustClose);

                        _reportedExceptions.Add(reportedException);

                        Interlocked.Increment(ref _reportWindowCount);

                        Process p = new Process();
                        p.StartInfo.FileName = Assembly.GetExecutingAssembly().Location;
                        p.StartInfo.Arguments = SerializationUtilities.ToBase64(data);
                        p.Exited += new EventHandler((object sender, EventArgs e) =>
                            {
                                Interlocked.Decrement(ref _reportWindowCount);

                                //i want a "reportedException" to be removed from the list when the window is closed.

                                //this permits the exact same exception to trigger a new window only when the previous associated window is closed.

                                for (int i = 0; i < _reportedExceptions.Count; i++)
                                {
                                    if (_reportedExceptions[i].IsEquivalent(data.Exception))
                                    {
                                        _reportedExceptions.RemoveAt(i);

                                        //i = Math.Max(0, i - 1); //more cautious to check all exceptions
                                        return; //more trustworthy to assume only 1 will match
                                    }
                                }
                            });
                        p.EnableRaisingEvents = true;
                        p.Start();
                    }
                }

                if (programMustClose)
                    ShutdownProgram();
            }
            catch (Exception e)
            {
#if RELEASE
                return //a release build shouldn't display this exception because it was the act of reporting that caused it.
#else
                EmergencyReporting(e, programMustClose);
#endif
            }
#endif
        }

        /// <summary>
        /// Displays an exception at all cost.
        /// </summary>
        /// <param name="reportedException"></param>
        /// <param name="programMustClose"></param>
        private void EmergencyReporting(Exception reportedException, bool programMustClose)
        {
            try
            {
                DefaultExceptionWindowBehavior defaultBehavior = new DefaultExceptionWindowBehavior();

                bool displayExceptionWindow;
                defaultBehavior.OnException(reportedException, out displayExceptionWindow);

                ExceptionWindowData data = new ExceptionWindowData(reportedException, defaultBehavior, programMustClose);

                Interlocked.Increment(ref _reportWindowCount);

                Process p = new Process();
                p.StartInfo.FileName = Assembly.GetExecutingAssembly().Location;
                p.StartInfo.Arguments = SerializationUtilities.ToBase64(data);
                p.Exited += new EventHandler((object sender, EventArgs e) =>
                    {
                        Interlocked.Decrement(ref _reportWindowCount);

                        //i want a "reportedException" to be removed from the list when the window is closed.

                        //this permits the exact same exception to trigger a new window only when the previous associated window is closed.

                        for (int i = 0; i < _reportedExceptions.Count; i++)
                        {
                            if (_reportedExceptions[i].IsEquivalent(data.Exception))
                            {
                                _reportedExceptions.RemoveAt(i);

                                //i = Math.Max(0, i - 1); //more cautious to check all exceptions
                                return; //more trustworthy to assume only 1 will match
                            }
                        }
                    });
                p.EnableRaisingEvents = true;
                p.Start();
            }

            catch (Exception ex)
            {
                StringBuilder errorMessage = new StringBuilder();
                errorMessage.AppendLine("An error has occurred while attempting to report another error.\n");

                errorMessage.AppendLine(">> Press Control + C to copy text <<\n");

                errorMessage.AppendLine("Program: " + Application.ProductName + " [ " + EntryAssemblyPath + " ]\n");

                errorMessage.AppendLine("Timestamp: " + DateTime.UtcNow.ToString("MM/dd/yyyy HH:mm:ss:ffff UTC") + Environment.NewLine);

                errorMessage.AppendLine("--------------------------");
                errorMessage.AppendLine("- Exception in program -");
                errorMessage.AppendLine("--------------------------\n");

                errorMessage.AppendLine(reportedException.ToDisplayString(false) + Environment.NewLine);

                errorMessage.AppendLine("------------------------------");
                errorMessage.AppendLine("- Exception while reporting -");
                errorMessage.AppendLine("------------------------------\n");

                errorMessage.AppendLine(ex.ToDisplayString(false));

                MessageBoxOptions localizedOptions;

                if (CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft)
                    localizedOptions = MessageBoxOptions.RightAlign | MessageBoxOptions.RtlReading;
                else
                    localizedOptions = (MessageBoxOptions)0; // Unfortunately, the MessageBoxOptions enum doesn't have a predefined ".None" value.

                Interlocked.Increment(ref _reportWindowCount);

                DialogResult result = MessageBox.Show(
                    errorMessage.ToString(),
                    "Error",
                    MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1, localizedOptions);

                if (result == DialogResult.OK)
                    Interlocked.Decrement(ref _reportWindowCount);
            }

            if (programMustClose)
                ShutdownProgram();
        }

        private void ShutdownProgram()
        {
#if BREAK_IN_EXCEPTION_REPORTER
            if (Debugger.IsAttached)
                Debugger.Break();
#endif
            while (_reportWindowCount != 0)
            {
                Thread.Sleep(500);

                //Application.DoEvents();
            }

            //by shutting down the program ourselves we prevent windows from handling the situation with the default popup:
            //"<program name> has stopped working. Windows is checking for a solution to the problem... Debug or close program?"

            Application.Exit(); //Closes any windows associated with the ExceptionReporter that are still open

            Environment.Exit(1); //Closes the main program that spawned the exception reporter. A non-zero value indicates an error occured.
        }

        #endregion Members
    }
}